File: /var/www/kevin-demo/wp-content/plugins/allspice/includes/content-badges.php
<?php
// includes/content-badges.php
//
// Lock badges for whole-content-gated posts, on the PUBLISHER'S listings (home/archive/
// search/related/widget cards) - never inside the Allspice widget, and never on the current
// singular post's own hero image.
//
// DOCUMENTED CHOICE: the badge is RETAINED for authorized members in listings - a premium
// item stays visibly premium, which is the recommendation this module follows.
//
// V1 hooks post_thumbnail_html only. Themes that build card images without
// get_the_post_thumbnail()/the_post_thumbnail() bypass this filter and will need a small
// theme-specific adapter later; a broad client-side DOM scanner is not built; add a theme-specific adapter instead.
if (!defined('ABSPATH')) exit;
/* Synced lock_badges settings, from the ONE normalizer (schema v2). Legacy configs carry no
lock_badges object; they keep the historical default-on behavior so already-deployed sites
don't lose badges mid-migration. v2 configs require enabled === true exactly. */
function allspice_lock_badge_config(): array {
if (function_exists('allspice_memberships_normalized')) {
$n = allspice_memberships_normalized();
$badges = $n['lock_badges'];
if ($n['legacy'] === true) {
/* Old schema never shipped lock_badges: retain the prior always-on default. */
$badges['enabled'] = true;
}
return $badges;
}
return ['enabled' => true, 'label' => 'Members', 'background_color' => null];
}
function allspice_lock_badge_enabled(): bool {
return (bool)apply_filters('allspice_lock_badge_enabled', allspice_lock_badge_config()['enabled'] === true);
}
function allspice_lock_badge_label(): string {
$label = trim((string)allspice_lock_badge_config()['label']);
if ($label === '') $label = 'Members';
return (string)apply_filters('allspice_lock_badge_label', $label);
}
/* Sanitized (strict color sanitizer) or null -> the stylesheet default. */
function allspice_lock_badge_background_color() {
return allspice_lock_badge_config()['background_color'];
}
/* 'top-right' (default) or 'bottom-right'. */
function allspice_lock_badge_position(): string {
$pos = (string)apply_filters('allspice_lock_badge_position', 'top-right');
return $pos === 'bottom-right' ? 'bottom-right' : 'top-right';
}
function allspice_lock_badge_class(): string {
return trim((string)apply_filters('allspice_lock_badge_class', ''));
}
add_filter('post_thumbnail_html', 'allspice_lock_badge_thumbnail', 20, 5);
function allspice_lock_badge_thumbnail($html, $post_id = 0, $post_thumbnail_id = 0, $size = '', $attr = '') {
if (!is_string($html) || $html === '' || !allspice_lock_badge_enabled()) return $html;
/* Fail open with memberships disabled/not ready - no badges from a stale snapshot. */
if (!function_exists('allspice_gate_memberships_active') || !allspice_gate_memberships_active()) return $html;
/* Never on the current singular post's primary hero. */
if (is_singular() && is_main_query() && (int)get_queried_object_id() === (int)$post_id) return $html;
/* Never twice. */
if (strpos($html, 'allspice-lock-badge') !== false) return $html;
/* Only whole-content gates: recipe_card-only and public posts have no rule here. */
if (!function_exists('allspice_content_gate_for_post')) return $html;
$rule = allspice_content_gate_for_post((int)$post_id);
if (!is_array($rule)) return $html;
$label = allspice_lock_badge_label();
$classes = 'allspice-lock-badge allspice-lock-badge--' . allspice_lock_badge_position();
$extra = allspice_lock_badge_class();
if ($extra !== '') $classes .= ' ' . $extra;
$badge = '<span class="' . esc_attr($classes) . '">'
. '<svg aria-hidden="true" viewBox="0 0 24 24" width="12" height="12" focusable="false">'
. '<path fill="currentColor" d="M12 2a5 5 0 0 0-5 5v3H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8a2 2 0 0 0-2-2h-1V7a5 5 0 0 0-5-5zm-3 8V7a3 3 0 1 1 6 0v3H9z"/></svg>'
. '<span class="allspice-lock-badge__label" aria-hidden="true">' . esc_html($label) . '</span>'
. '<span class="screen-reader-text allspice-lock-badge__sr">' . esc_html($label) . ' - members-only content</span>'
. '</span>';
/* Wrap image + badge in a positioned span, so any theme link AROUND the thumbnail keeps
working and the badge anchors to the image box. */
return '<span class="allspice-lock-badge-wrap">' . $html . $badge . '</span>';
}
/* Badge CSS rides the existing always-loaded gate stylesheet handle. */
add_action('wp_enqueue_scripts', 'allspice_lock_badge_styles', 21);
function allspice_lock_badge_styles(): void {
if (!function_exists('allspice_gate_memberships_active') || !allspice_gate_memberships_active()) return;
if (!allspice_lock_badge_enabled()) return;
/* HOST-THEME ARMOR (live finding, 2026-08-26): the badge renders inside the theme's own
thumbnail link, where `a span { color/line-height }` rules outrank a single class.
Every property a theme plausibly styles on links and spans is pinned per element with
!important - the same per-element armor rule the inline widget uses. */
wp_add_inline_style('allspice-gate',
'.allspice-lock-badge-wrap{position:relative;display:inline-block;max-width:100%}'
. '.allspice-lock-badge{position:absolute;display:inline-flex!important;align-items:center!important;gap:4px;'
. 'padding:3px 8px;border-radius:999px;background:'
. (allspice_lock_badge_background_color() !== null ? allspice_lock_badge_background_color() : 'rgba(20,22,28,.82)')
. ';color:#fff!important;'
. 'font-size:11px!important;font-weight:600;line-height:1!important;letter-spacing:.02em;'
. 'text-decoration:none!important;pointer-events:none;z-index:2;transition:opacity .18s ease}'
. '.allspice-lock-badge__label{color:#fff!important;font-size:11px!important;line-height:1!important;display:block}'
. '.allspice-lock-badge--top-right{top:8px;right:8px}'
. '.allspice-lock-badge--bottom-right{bottom:8px;right:8px}'
. '.allspice-lock-badge svg{display:block;width:12px;height:12px;flex:0 0 auto;color:#fff!important}'
/* Applied by the loader script below while the badge is anchored to a still-collapsed
lazy image box; removed on load/error. Without JS it never applies (badges visible). */
. '.allspice-lock-badge--waiting{opacity:0}'
. '.allspice-lock-badge .screen-reader-text{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}'
);
}
/*
* A lazy thumbnail with no reserved height collapses the wrap the badge anchors to, so the
* badge sat at the wrong spot until the image arrived and then jumped into place. The badge
* defaults VISIBLE (no-JS keeps badges); this only HIDES badges whose image is genuinely
* still loading and reveals them (with a short fade) the moment it lands or fails.
*/
add_action('wp_footer', 'allspice_lock_badge_loading_script', 5);
function allspice_lock_badge_loading_script(): void {
if (!function_exists('allspice_gate_memberships_active') || !allspice_gate_memberships_active()) return;
if (!allspice_lock_badge_enabled()) return;
echo '<script>(function(){var m=function(w){var i=w.querySelector("img"),b=w.querySelector(".allspice-lock-badge");'
. 'if(!i||!b)return;'
. 'if(i.complete&&i.naturalWidth>0){b.classList.remove("allspice-lock-badge--waiting");return;}'
. 'b.classList.add("allspice-lock-badge--waiting");'
. 'var d=function(){b.classList.remove("allspice-lock-badge--waiting");};'
. 'i.addEventListener("load",d,{once:true});i.addEventListener("error",d,{once:true});};'
. 'var boot=function(){document.querySelectorAll(".allspice-lock-badge-wrap").forEach(m);};'
. 'if(document.readyState==="loading"){document.addEventListener("DOMContentLoaded",boot);}else{boot();}'
. '})();</script>' . "\n";
}